//--------------------------------------------------------------------------
// 
//  Copyright (c) Chili Software.  All rights reserved. 
// 
//  File: DynamicPopulateHelperMetaInfo.cs
//
//  Description: A mapped member when implementing IDynamicPopulateHelper.
// 
//--------------------------------------------------------------------------

using System;
using System.Reflection;
using System.Reflection.Emit;

namespace Chili.Opf3.Mapping
{
	/// <summary>
	/// Class represents a meta element that is used to create the mapping between the persistent object and
	/// the field in the table.
	/// </summary>
	/// <remarks>
	/// This class represents a meta element that is used to create the mapping between a field
	/// of the persistent object and a field in the storage table. This class uses the <see cref="IDynamicPopulateHelper">IDynamicPopulateHelper</see>
	/// interface to set and get the values in/from the persistent's members.
	/// </remarks>
	internal sealed class DynamicPopulateHelperMetaInfo : MetaInfo
	{
		private readonly string NAME;
		private Type _enumType;
		private PropertySetDelegate _setter;
		private PropertyGetDelegate _getter;

		/// <summary>
		/// Creates a new instance of the <see cref="DynamicPopulateHelperMetaInfo">DynamicPopulateHelperMetaInfo Class</see>.
		/// </summary>
		/// <param name="memberInfo">The MemberInfo object that represents the member in the persistent object's type.</param>
		/// <param name="type">Specifies the type of the member.</param>
		/// <param name="attribute">The <see cref="FieldAttribute">FieldAttribute</see> that contains the mapping information.</param>
		public DynamicPopulateHelperMetaInfo(MemberInfo memberInfo, Type type, FieldAttribute attribute)
			: base(type, memberInfo, attribute)
		{
			// Check and get the underlying enum type.
			if (this.MemberType.IsEnum)
				_enumType = Enum.GetUnderlyingType(this.MemberType);

			NAME = memberInfo.Name;

			EmitMethods(memberInfo);
		}

		/// <summary>
		/// Emits the methods to the type.
		/// </summary>
		/// <param name="memberInfo">The member that is accessed via the method.</param>
		private void EmitMethods(MemberInfo memberInfo)
		{
			if (memberInfo == null)
				throw new ArgumentNullException("memberInfo");

			PropertyInfo property = (PropertyInfo)memberInfo;
			// Emit the get method.
			EmitGetMethod(property);
			// Emit the set method.
			EmitSetMethod(property);
		}

		/// <summary>
		/// Emits the set method for the member in the type.
		/// </summary>
		/// <param name="property"></param>
		private void EmitSetMethod(PropertyInfo property)
		{
			// Don't create the set method for user defined types!
			if (IsUserDefinedType)
				return;

			// Create the dynamic method.
			DynamicMethod dynmethod = new DynamicMethod("Chili_Opf3_DynamicSet$" + property.Name, null, 
				new Type[] { typeof(object), typeof(object) }, property.DeclaringType);

			// Get the property getter.
			MethodInfo method = property.GetSetMethod(true);
			if (method == null)
			{
				throw new MappingException(string.Format("Cannot create mapping for {0}.{1} because it's not possible to write and read.",
					property.ReflectedType.FullName, property.Name));
			}

			// Create the code generator for the method.
			ILGenerator il = dynmethod.GetILGenerator();

			// Check if nullable and an enum. They need special treathment...
			// The combination nullable + enum seems to cause problems.
			if (this.IsNullable && this.MemberType.BaseType == typeof(Enum))
			{
				Type t = typeof(Nullable<>).MakeGenericType(this.MemberType);

				// Create a local variable that holds the value that is set.
				LocalBuilder local0 = il.DeclareLocal(t);

				// Load the second argument.
				il.Emit(OpCodes.Ldarg_1);
				// Check the second argument with null.
				il.Emit(OpCodes.Ldnull);
				il.Emit(OpCodes.Ceq);

				// Define a label and branch to the label if 1 is on the stack.
				// In this case if the value for the property is null.
				Label label1 = il.DefineLabel();
				il.Emit(OpCodes.Brtrue_S, label1);

				// Load the second argument.
				il.Emit(OpCodes.Ldarg_1);
				// Unbox. This is always an enum or null.
				il.Emit(OpCodes.Unbox_Any, this.MemberType);

				// Create the instance of the nullabe and give it the current value on the second argument.
				ConstructorInfo constructor = t.GetConstructor(new Type[] { this.MemberType });
				il.Emit(OpCodes.Newobj, constructor);
				// Store the result.
				il.Emit(OpCodes.Stloc_0);

				// The first label.
                il.MarkLabel(label1);

				// Load the first argument.
				il.Emit(OpCodes.Ldarg_0);
				// Cast it to the properties declaring type.
				il.Emit(OpCodes.Castclass, property.DeclaringType);
				// Load the value in the first local variable.
				il.Emit(OpCodes.Ldloc_0);
				// Invoked the method.
				il.EmitCall(OpCodes.Callvirt, method, null);
			}
			else
			{
				// Load the first argument.
				il.Emit(OpCodes.Ldarg_0);
				// Cast it to the properties declaring type.
				il.Emit(OpCodes.Castclass, property.DeclaringType);
				// Load the second argument.
				il.Emit(OpCodes.Ldarg_1);

				// Unbox or cast if required.
				if (property.PropertyType.IsClass)
					il.Emit(OpCodes.Castclass, property.PropertyType);
				else
					il.Emit(OpCodes.Unbox_Any, property.PropertyType);
				
				// Invoked the method.
				il.EmitCall(OpCodes.Callvirt, method, null);
			}

			// Return.
			il.Emit(OpCodes.Ret);
			// Create the delegate for the method.
			_setter = (PropertySetDelegate)dynmethod.CreateDelegate(typeof(PropertySetDelegate));
		}

		/// <summary>
		/// Emits the get method for the member in the type.
		/// </summary>
		/// <param name="property"></param>
		private void EmitGetMethod(PropertyInfo property)
		{
			// Create the dynamic method.
			DynamicMethod dynmethod = new DynamicMethod("Chili_Opf3_DynamicGet$" + property.Name, typeof(object), 
				new Type[] { typeof(object) }, property.DeclaringType);

			// Get the property getter.
			MethodInfo method = property.GetGetMethod(true);
			if (method == null)
			{
				throw new MappingException(string.Format("Cannot create mapping for {0}.{1} because it's not possible to write and read.",
					property.ReflectedType.FullName, property.Name));
			}

			// Create the code generator for the method.
			ILGenerator il = dynmethod.GetILGenerator(256);

			// Load the first argument on the stack.
			il.Emit(OpCodes.Ldarg_0);
			// Cast the first argument to the properties declaring type.
			il.Emit(OpCodes.Castclass, property.DeclaringType);
			// Call the method.
			il.EmitCall(OpCodes.Callvirt, method, null);

			// Box if required.
			if (!method.ReturnType.IsClass)
				il.Emit(OpCodes.Box, property.PropertyType);

			// Return.
			il.Emit(OpCodes.Ret);

			// Create the delegate for the method.
			_getter = (PropertyGetDelegate)dynmethod.CreateDelegate(typeof(PropertyGetDelegate));
		}

		/// <summary>
		/// Returns the name of the element of the persistent object type that is mapped to the field in the storage.
		/// </summary>
		public override string Name
		{
			get { return NAME; }
		}

		/// <summary>
		/// Returns the value of the element of the persistent object type that is mapped to a field in the storage.
		/// </summary>
		/// <param name="obj">The persistent object that's value is returned.</param>
		/// <returns>Value of the persistent object types's element that is mapped to a field in the storage.</returns>
		public override object GetValue(object obj)
		{
			object val = _getter(obj);

			if (IsUserDefinedType && val != null)
				val = ((IUserDefinedType)val).ToSimpleDataType();

			return val;
		}

		/// <summary>
		/// Sets new value for the element of the persistent object type that is mapped to the storage.
		/// </summary>
		/// <param name="obj">The persistent object that's value is updated.</param>
		/// <param name="value">The new value.</param>
		public override void SetValue(object obj, object value)
		{
			if (IsUserDefinedType)
				((IUserDefinedType)_getter(obj)).FromSimpleDataType(value);
			else
			{
				// Get the type.
				Type t = (_enumType != null) ? _enumType : this.MemberType;

				// Convert the value.
				if (value != null && value.GetType() != t)
					value = Convert.ChangeType(value, t);

				_setter(obj, value);
			}
		}
	}
}
